package de.saring.sportstracker.gui.dialogs;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import javax.inject.Inject;
import de.saring.sportstracker.data.EntryFilter;
import de.saring.sportstracker.data.EntryList;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.fx.ChartViewer;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.time.Month;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimeTableXYDataset;
import org.jfree.data.time.Year;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
import de.saring.sportstracker.core.STOptions;
import de.saring.sportstracker.data.Equipment;
import de.saring.sportstracker.data.Exercise;
import de.saring.sportstracker.data.SportSubType;
import de.saring.sportstracker.data.SportType;
import de.saring.sportstracker.data.Weight;
import de.saring.sportstracker.gui.STContext;
import de.saring.sportstracker.gui.STDocument;
import de.saring.util.AppResources;
import de.saring.util.Date310Utils;
import de.saring.util.gui.javafx.ColorUtils;
import de.saring.util.gui.javafx.NameableStringConverter;
import de.saring.util.gui.jfreechart.ChartUtils;
import de.saring.util.gui.jfreechart.StackedRenderer;
import de.saring.util.unitcalc.ConvertUtils;
import de.saring.util.unitcalc.FormatUtils;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Window;
/**
* Controller (MVC) class of the Overview dialog of the SportsTracker application.
* This dialog contains a diagram which displays all the exercises or weight entries in
* various diagram graph types. The user can select the displayed time range (e.g. all
* months of the selected year or the last 10 years until the selected year).
*
* @author Stefan Saring
*/
public class OverviewDialogController extends AbstractDialogController {
private final STDocument document;
/** The viewer for the chart. */
private ChartViewer chartViewer;
@FXML
private ChoiceBox<TimeRangeType> cbTimeRange;
@FXML
private Spinner<Integer> spYear;
@FXML
private ChoiceBox<ValueType> cbDisplay;
@FXML
private ChoiceBox<OverviewType> cbSportTypeMode;
@FXML
private ChoiceBox<SportType> cbSportTypeList;
@FXML
private StackPane spDiagram;
@FXML
private HBox hBoxOptions;
@FXML
private HBox hBoxSportTypeMode;
@FXML
private HBox hBoxSportTypeList;
/**
* Standard c'tor for dependency injection.
*
* @param context the SportsTracker UI context
* @param document the SportsTracker model/document
*/
@Inject
public OverviewDialogController(final STContext context, final STDocument document) {
super(context);
this.document = document;
TimeRangeType.appResources = context.getResources();
ValueType.appResources = context.getResources();
OverviewType.appResources = context.getResources();
}
/**
* Displays the Overview dialog.
*
* @param parent parent window of the dialog
*/
public void show(final Window parent) {
// display in title when exercise filter is being used
String dlgTitle = context.getResources().getString("st.dlg.overview.title");
if (isExerciseFilterEnabled()) {
dlgTitle += " " + context.getResources().getString("st.dlg.overview.title.filter");
}
showInfoDialog("/fxml/dialogs/OverviewDialog.fxml", parent, dlgTitle);
}
@Override
protected void setupDialogControls() {
setupChoiceBoxes();
updateDiagram();
}
private void setupChoiceBoxes() {
// fill choice boxes
cbTimeRange.getItems().addAll(Arrays.asList(TimeRangeType.values()));
cbTimeRange.getSelectionModel().select(TimeRangeType.LAST_12_MONTHS);
cbDisplay.getItems().addAll(Arrays.asList(ValueType.values()));
cbDisplay.getSelectionModel().select(ValueType.DISTANCE);
cbSportTypeMode.getItems().addAll(Arrays.asList(OverviewType.values()));
cbSportTypeMode.getSelectionModel().select(OverviewType.EACH_SPLITTED);
cbSportTypeList.setConverter(new NameableStringConverter<>());
document.getSportTypeList().forEach(sportType -> cbSportTypeList.getItems().add(sportType));
cbSportTypeList.getSelectionModel().select(0);
// init spinner for year selection, must not be visible for time range type "last 12 months"
spYear.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1950, 2070, LocalDate.now().getYear()));
spYear.visibleProperty().bind(Bindings.notEqual(cbTimeRange.valueProperty(), TimeRangeType.LAST_12_MONTHS));
// add mouse wheel support for the year spinner (not done by default)
// TODO remove when my Tweak Request for JavaFx has been impplemented
// https://javafx-jira.kenai.com/browse/RT-40269
spYear.setOnScroll(event -> {
if (event.getDeltaY() > 0) {
spYear.decrement();
} else if (event.getDeltaY() < 0) {
spYear.increment();
}
});
// set listeners for updating the diagram on selection changes
cbTimeRange.addEventHandler(ActionEvent.ACTION, event -> updateDiagram());
spYear.valueProperty().addListener((observable, oldValue, newValue) -> updateDiagram());
cbDisplay.addEventHandler(ActionEvent.ACTION, event -> updateDiagram());
cbSportTypeMode.addEventHandler(ActionEvent.ACTION, event -> updateDiagram());
cbSportTypeList.addEventHandler(ActionEvent.ACTION, event -> updateDiagram());
}
/**
* Draws the Overview diagram according to the current selections.
*/
private void updateDiagram() {
updateOptionControls();
// get selected time range and value type and its name to display
TimeRangeType timeType = cbTimeRange.getValue();
ValueType vType = cbDisplay.getValue();
String valueTypeNameWithUnits = vType.getNameWithUnitSystem(context.getFormatUtils());
int year = spYear.getValue();
// create a table of all time series (graphs) and the appropriate colors
TimeTableXYDataset dataset = new TimeTableXYDataset();
java.util.List<java.awt.Color> lGraphColors = new ArrayList<>();
// setup TimeSeries in the diagram (done in different ways for all the value types)
if (vType == ValueType.SPORTSUBTYPE) {
setupSportSubTypeDiagram(dataset, lGraphColors);
} else if (vType == ValueType.EQUIPMENT) {
setupEquipmentDiagram(dataset, lGraphColors);
} else if (vType == ValueType.WEIGHT) {
setupWeightDiagram(dataset, lGraphColors);
} else {
setupExerciseDiagram(dataset, lGraphColors);
}
// create chart
JFreeChart chart = ChartFactory.createTimeSeriesChart( //
null, // Title
null, // Y-axis label
valueTypeNameWithUnits, // X-axis label
dataset, // primary dataset
true, // display legend
true, // display tooltips
false); // URLs
// render unique filled shapes for each graph
XYPlot plot = (XYPlot) chart.getPlot();
XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) plot.getRenderer();
renderer.setBaseShapesVisible(true);
renderer.setBaseShapesFilled(true);
// set color for sport type series
for (int i = 0; i < lGraphColors.size(); i++) {
java.awt.Color tempColor = lGraphColors.get(i);
renderer.setSeriesPaint(i, tempColor);
}
// setup date format for tooltips and time (bottom) axis
String dateFormatTooltip;
String dateFormatAxis;
DateTickUnit dateTickUnit;
switch (timeType) {
case LAST_12_MONTHS:
dateFormatTooltip = "MMMMM yyyy";
dateFormatAxis = "MMM";
dateTickUnit = new DateTickUnit(DateTickUnitType.MONTH, 1);
break;
case MONTHS_OF_YEAR:
dateFormatTooltip = "MMMMM";
dateFormatAxis = "MMM";
dateTickUnit = new DateTickUnit(DateTickUnitType.MONTH, 1);
break;
case WEEKS_OF_YEAR:
// Workaround for a JFreeChart formating problem: years are used
// instead of weeks for the bottom axis, otherwise there will be
// format problems on the axis (the first week is often "52")
dateFormatTooltip = "yy";
dateFormatAxis = "yy";
dateTickUnit = new DateTickUnit(DateTickUnitType.YEAR, 2);
break;
default: // LAST_10_YEARS
dateFormatTooltip = "yyyy";
dateFormatAxis = "yyyy";
dateTickUnit = new DateTickUnit(DateTickUnitType.YEAR, 1);
break;
}
// setup tooltips: must display month, week or year and the value only
String toolTipFormat = "{1}: {2}";
renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator(toolTipFormat, new SimpleDateFormat(
dateFormatTooltip), new DecimalFormat()) {
@Override
public String generateToolTip(XYDataset dataset, int series, int item) {
return dataset.getSeriesKey(series) + ", " + super.generateToolTip(dataset, series, item);
}
});
// special handling for overview type EACH_STACKED, it uses a special renderer
// (only for exercises based value types, stacked mode can be selected only there)
if (cbSportTypeMode.isVisible() && cbSportTypeMode.getValue() == OverviewType.EACH_STACKED) {
renderer.setSeriesLinesVisible(0, false);
renderer.setSeriesShapesVisible(0, false);
renderer.setSeriesVisibleInLegend(0, false);
// actual dataset
dataset = new TimeTableXYDataset();
lGraphColors = new ArrayList<>();
// create a separate graph for each sport type
for (SportType sportType : document.getSportTypeList()) {
addExerciseTimeSeries(dataset, timeType, year, vType, sportType);
lGraphColors.add(ColorUtils.toAwtColor(sportType.getColor()));
}
plot.setDataset(1, dataset);
// actual stacked renderer
StackedRenderer stackedRenderer = new StackedRenderer();
// set color for sport type series
for (int i = 0; i < lGraphColors.size(); i++) {
java.awt.Color tempColor = lGraphColors.get(i);
stackedRenderer.setSeriesPaint(i, tempColor);
}
stackedRenderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator(toolTipFormat, new SimpleDateFormat(
dateFormatTooltip), new DecimalFormat()) {
@Override
public String generateToolTip(XYDataset dataset, int series, int item) {
return dataset.getSeriesKey(series) + ", " + super.generateToolTip(dataset, series, item);
}
});
plot.setRenderer(1, stackedRenderer);
}
// set date format of time (bottom) axis
DateAxis axis = (DateAxis) plot.getDomainAxis();
axis.setDateFormatOverride(new SimpleDateFormat(dateFormatAxis));
if (dateTickUnit != null) {
axis.setTickUnit(dateTickUnit);
}
// set value range from 0 to 10 when there are no values
// (otherwise the range is double minimum to double maximum)
if (plot.getRangeAxis().getRange().getCentralValue() == 0d) {
plot.getRangeAxis().setRange(new Range(0, 10));
}
// add vertical year break marker when displaying the last 12 months
if (timeType == TimeRangeType.LAST_12_MONTHS) {
LocalDate firstDayOfYear = LocalDate.now().with(TemporalAdjusters.firstDayOfYear());
LocalDate middleOfDecemberOfLastYear = firstDayOfYear.minusDays(15);
Date dateMiddleOfDecemberOfLastYear = Date310Utils.localDateToDate(middleOfDecemberOfLastYear);
ValueMarker newYearMarker = new ValueMarker(dateMiddleOfDecemberOfLastYear.getTime());
newYearMarker.setPaint(new java.awt.Color(0x00b000));
newYearMarker.setStroke(new java.awt.BasicStroke(0.8f));
newYearMarker.setLabel(String.valueOf(firstDayOfYear.getYear()));
newYearMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
newYearMarker.setLabelTextAnchor(TextAnchor.TOP_LEFT);
plot.addDomainMarker(newYearMarker);
}
// display legend next to the diagram on the right side
chart.getLegend().setPosition(RectangleEdge.RIGHT);
ChartUtils.customizeChart(chart);
// display chart in viewer (chart viewer will be initialized lazily)
if (chartViewer == null) {
chartViewer = new ChartViewer(chart);
spDiagram.getChildren().addAll(chartViewer);
} else {
chartViewer.setChart(chart);
}
}
private void updateOptionControls() {
final ValueType selectedValueType = cbDisplay.getValue();
// the sport type mode selection must not be visible for the ValueType SPORTSUBTYPE, EQUIPMENT and WEIGHT
final boolean sportTypeModeVisible = selectedValueType != ValueType.SPORTSUBTYPE
&& selectedValueType != ValueType.EQUIPMENT && selectedValueType != ValueType.WEIGHT;
// the sport type list selection must only be visible for the ValueType SPORTSUBTYPE and EQUIPMENT
final boolean sportTypeListVisible = selectedValueType == ValueType.SPORTSUBTYPE
|| selectedValueType == ValueType.EQUIPMENT;
// add or remove sport type mode selection depending on visibility and current state
if (sportTypeModeVisible && !hBoxOptions.getChildren().contains(hBoxSportTypeMode)) {
hBoxOptions.getChildren().add(hBoxSportTypeMode);
}
if (!sportTypeModeVisible && hBoxOptions.getChildren().contains(hBoxSportTypeMode)) {
hBoxOptions.getChildren().remove(hBoxSportTypeMode);
}
// add or remove sport type list selection depending on visibility and current state
if (sportTypeListVisible && !hBoxOptions.getChildren().contains(hBoxSportTypeList)) {
hBoxOptions.getChildren().add(hBoxSportTypeList);
}
if (!sportTypeListVisible && hBoxOptions.getChildren().contains(hBoxSportTypeList)) {
hBoxOptions.getChildren().remove(hBoxSportTypeList);
}
}
/**
* Sets up the diagram for exercise data.
*
* @param dataset the XY dataset to be filled
* @param graphColors list of graph colors, can be filled with preferred colors
*/
private void setupExerciseDiagram(TimeTableXYDataset dataset, java.util.List<java.awt.Color> graphColors) {
// get time range and value type to display
TimeRangeType timeType = cbTimeRange.getValue();
int year = spYear.getValue();
ValueType vType = cbDisplay.getValue();
OverviewType overviewType = cbSportTypeMode.getValue();
// which sport type mode is selected by user ?
if (overviewType != OverviewType.EACH_SPLITTED) {
// create one graph for sum of all sport types
addExerciseTimeSeries(dataset, timeType, year, vType, null);
graphColors.add(new java.awt.Color(0xff0000));
} else {
// create a separate graph for each sport type
for (SportType sportType : document.getSportTypeList()) {
addExerciseTimeSeries(dataset, timeType, year, vType, sportType);
graphColors.add(ColorUtils.toAwtColor(sportType.getColor()));
}
}
}
/**
* This method calculates the specified exercise values (distance, duration, ascent,
* avarage speed or calories concumption) and adds them to a TimeTableXYDataset.
* The calculation can be done for the exercises of all sport types (sum) or for a
* single sport type.
*
* @param dataset the timetable dataset
* @param timeType time range for calculated values
* @param year the year for calculation
* @param valueType the type of values needs to be calculated
* @param sportType the specific sport type to be calculated or null for the sum of all sport types
*/
private void addExerciseTimeSeries(TimeTableXYDataset dataset, TimeRangeType timeType, int year,
ValueType valueType, SportType sportType) {
// create the time series for specified time range and sport type
String seriesName = sportType != null ? sportType.getName() : context.getResources().getString(
"st.dlg.overview.graph.all_types");
// process value calculation for each step of time range
int timeStepCount = getTimeStepCount(timeType, year);
for (int timeStep = 0; timeStep < timeStepCount; timeStep++) {
// create time period for current time step
RegularTimePeriod timePeriod = createTimePeriodForTimeStep(timeType, year, timeStep);
// create the EntryFilter for the time range of the current time step
EntryFilter filter = createExerciseFilterForTimeStep(timeType, year, timeStep);
filter.setSportType(sportType);
mergeExerciseFilterIfEnabled(filter);
// get exercises for defined filter
// (add value 0 and skip to next time step when no exercises found)
EntryList<Exercise> lExercises = document.getExerciseList().getEntriesForFilter(filter);
if (lExercises.size() == 0) {
dataset.add(timePeriod, 0, seriesName);
continue;
}
// create sums of all exercises
double sumDistance = 0d;
double sumDuration = 0d;
double sumAscent = 0d;
double sumCalories = 0d;
for (Exercise tempExercise : lExercises) {
sumDistance += tempExercise.getDistance();
sumDuration += tempExercise.getDuration();
sumAscent += tempExercise.getAscent();
sumCalories += tempExercise.getCalories();
}
// set value of time step depending on value type
// (convert to english unit mode when enabled)
STOptions options = document.getOptions();
switch (valueType) {
case DISTANCE:
if (options.getUnitSystem() != FormatUtils.UnitSystem.Metric) {
sumDistance = ConvertUtils.convertKilometer2Miles(sumDistance, false);
}
dataset.add(timePeriod, sumDistance, seriesName);
break;
case DURATION:
// calculate duration in hours
dataset.add(timePeriod, sumDuration / 3600d, seriesName);
break;
case ASCENT:
if (options.getUnitSystem() != FormatUtils.UnitSystem.Metric) {
sumAscent = ConvertUtils.convertMeter2Feet((int) sumAscent);
}
dataset.add(timePeriod, sumAscent, seriesName);
break;
case CALORIES:
// set calorie consumption
dataset.add(timePeriod, sumCalories, seriesName);
break;
case EXERCISES:
// set number of exercises
dataset.add(timePeriod, lExercises.size(), seriesName);
break;
case AVG_SPEED:
// calculate AVG speed of all exercises of time step
if (options.getUnitSystem() != FormatUtils.UnitSystem.Metric) {
sumDistance = ConvertUtils.convertKilometer2Miles(sumDistance, false);
}
double averageSpeed = sumDistance / (sumDuration / 3600d);
// calculate the speed value depending on current speed unit view
if (options.getSpeedView() == FormatUtils.SpeedView.MinutesPerDistance) {
if (averageSpeed == 0) {
dataset.add(timePeriod, 0, seriesName);
} else {
dataset.add(timePeriod, 60 / averageSpeed, seriesName);
}
} else {
dataset.add(timePeriod, averageSpeed, seriesName);
}
break;
default:
dataset.add(timePeriod, 0, seriesName);
}
}
}
/**
* Sets up the diagram for sport subtype overview for the selected sport type.
*
* @param dataset the XY dataset to be filled
* @param graphColors list of graph colors, can be filled with preferred colors
*/
private void setupSportSubTypeDiagram(TimeTableXYDataset dataset, java.util.List<java.awt.Color> graphColors) {
// get time range to display
TimeRangeType timeType = cbTimeRange.getValue();
int year = spYear.getValue();
// get selected sport type
SportType sportType = cbSportTypeList.getValue();
// display a graph for each sport subtype
for (SportSubType sportSubType : sportType.getSportSubTypeList()) {
addSportSubTypeTimeSeries(dataset, timeType, year, sportType, sportSubType);
}
addCustomGraphColors(graphColors);
}
/**
* This method calculates the distance per sport subtype values and adds them to a TimeTableXYDataset.
* The calculation is always done for the sport type selected by the user.
*
* @param dataset the timetable dataset
* @param timeType time range for calculated values
* @param year the year for calculation
* @param sportType the sport type to be shown
* @param sportSubType the sport subtype to be shown in this series
*/
private void addSportSubTypeTimeSeries(TimeTableXYDataset dataset, TimeRangeType timeType, int year,
SportType sportType, SportSubType sportSubType) {
String seriesName = sportSubType.getName();
// process value calculation for each step of time range
int timeStepCount = getTimeStepCount(timeType, year);
for (int timeStep = 0; timeStep < timeStepCount; timeStep++) {
// create time period for current time step
RegularTimePeriod timePeriod = createTimePeriodForTimeStep(timeType, year, timeStep);
// create the EntryFilter for the time range of the current time step
EntryFilter filter = createExerciseFilterForTimeStep(timeType, year, timeStep);
filter.setSportType(sportType);
filter.setSportSubType(sportSubType);
mergeExerciseFilterIfEnabled(filter);
// get exercises for defined filter
EntryList<Exercise> lExercises = document.getExerciseList().getEntriesForFilter(filter);
// create distance sum of all found exercises
double sumDistance = 0d;
for (Exercise tempExercise : lExercises) {
// sum only exercises with same sport subtype (otherwise conflicts with the merged filter set in the
// view)
if (sportSubType.equals(tempExercise.getSportSubType())) {
sumDistance += tempExercise.getDistance();
}
}
// convert to english unit mode when enabled
if (document.getOptions().getUnitSystem() != FormatUtils.UnitSystem.Metric) {
sumDistance = ConvertUtils.convertKilometer2Miles(sumDistance, false);
}
// set distance value of time step
dataset.add(timePeriod, sumDistance, seriesName);
}
}
/**
* Sets up the diagram for equipment usage for the selected sport type.
*
* @param dataset the XY dataset to be filled
* @param graphColors list of graph colors, can be filled with preferred colors
*/
private void setupEquipmentDiagram(TimeTableXYDataset dataset, java.util.List<java.awt.Color> graphColors) {
// get time range to display
TimeRangeType timeType = cbTimeRange.getValue();
int year = spYear.getValue();
// get selected sport type
SportType sportType = cbSportTypeList.getValue();
// display a graph for each equipment and one for not specified equipment
for (Equipment equipment : sportType.getEquipmentList()) {
addEquipmentTimeSeries(dataset, timeType, year, sportType, equipment);
}
addEquipmentTimeSeries(dataset, timeType, year, sportType, null);
addCustomGraphColors(graphColors);
}
/**
* This method calculates the distance per equipment values and adds them to a TimeTableXYDataset.
* The calculation is always done for the sport type selected by the user.
*
* @param dataset the timetable dataset
* @param timeType time range for calculated values
* @param year the year for calculation
* @param sportType the sport type to be shown
* @param equipment the equipment to be shown in this series (when null, then calculate exercises with no equipment
* assigned only)
*/
private void addEquipmentTimeSeries(TimeTableXYDataset dataset, TimeRangeType timeType, int year,
SportType sportType, Equipment equipment) {
String seriesName = equipment != null ? equipment.getName() : context.getResources().getString(
"st.dlg.overview.equipment.not_specified");
// process value calculation for each step of time range
int timeStepCount = getTimeStepCount(timeType, year);
for (int timeStep = 0; timeStep < timeStepCount; timeStep++) {
// create time period for current time step
RegularTimePeriod timePeriod = createTimePeriodForTimeStep(timeType, year, timeStep);
// create the EntryFilter for the time range of the current time step
EntryFilter filter = createExerciseFilterForTimeStep(timeType, year, timeStep);
filter.setSportType(sportType);
filter.setEquipment(equipment);
mergeExerciseFilterIfEnabled(filter);
// get exercises for defined filter
EntryList<Exercise> lExercises = document.getExerciseList().getEntriesForFilter(filter);
// create distance sum of all found exercises
double sumDistance = 0d;
for (Exercise tempExercise : lExercises) {
// when displaying series for no equipment assigned then skip exercises with assigned equipment
if (equipment == null && tempExercise.getEquipment() != null) {
continue;
}
sumDistance += tempExercise.getDistance();
}
// convert to english unit mode when enabled
if (document.getOptions().getUnitSystem() != FormatUtils.UnitSystem.Metric) {
sumDistance = ConvertUtils.convertKilometer2Miles(sumDistance, false);
}
// set distance value of time step
dataset.add(timePeriod, sumDistance, seriesName);
}
}
/**
* Sets up the diagram for weight data.
*
* @param dataset the XY dataset to be filled
* @param graphColors list of graph colors, can be filled with preferred colors
*/
private void setupWeightDiagram(TimeTableXYDataset dataset, java.util.List<java.awt.Color> graphColors) {
// get time range to display
TimeRangeType timeType = cbTimeRange.getValue();
int year = spYear.getValue();
addWeightTimeSeries(dataset, timeType, year);
graphColors.add(new java.awt.Color(0xff0000));
}
/**
* This method creates a TimeSeries graph which contains all Weight entries
* for the current selected time range and adds them to the passed
* TimeTableXYDataset.
*
* @param dataset the timetable dataset
* @param timeType time range for calculated values
* @param year the year for calculation
*/
private void addWeightTimeSeries(TimeTableXYDataset dataset, TimeRangeType timeType, int year) {
String seriesName = context.getResources().getString("st.dlg.overview.display.weight.text");
// process value calculation for each step of time range
int timeStepCount = getTimeStepCount(timeType, year);
for (int timeStep = 0; timeStep < timeStepCount; timeStep++) {
// create time period for current time step
RegularTimePeriod timePeriod = createTimePeriodForTimeStep(timeType, year, timeStep);
// create the EntryFilter for the time range of the current time step
// (EntryFilter was not made for Weight, but it's also handy to use here :-)
EntryFilter filter = createExerciseFilterForTimeStep(timeType, year, timeStep);
// get average weight for the time range of this step
double avgWeight = getAverageWeightInTimeRange(filter);
if (document.getOptions().getUnitSystem() != FormatUtils.UnitSystem.Metric) {
avgWeight = ConvertUtils.convertKilogram2Lbs(avgWeight);
}
// add computed value to time series (if the weight value is available)
if (avgWeight > 0) {
dataset.add(timePeriod, avgWeight, seriesName, true);
}
// when there is no weight value, add at least the first and last dataset item
// to make sure the full time range is shown
else if (timeStep == 0 || timeStep == timeStepCount - 1) {
dataset.add(timePeriod, (Number) null, seriesName, true);
}
}
}
/**
* Returns the number of displayed time steps in the specified time range type.
*
* @param timeType the time range type to be displayed
* @param year the year to be displayed
* @return number of time steps
*/
private int getTimeStepCount(TimeRangeType timeType, int year) {
switch (timeType) {
case LAST_12_MONTHS:
return 13;
case MONTHS_OF_YEAR:
return 12;
case WEEKS_OF_YEAR:
// Workaround for a JFreeChart problem: use the Year instead
// of Week class for the bottom axis, otherwise there will be
// format problems on the axis (the first week is often "52")
// => get number of weeks for the specified year (mostly 52, sometimes 53)
LocalDate dateinYear = LocalDate.of(year, 1, 15);
WeekFields weekField = getWeekFieldsForWeekStart();
return (int) dateinYear.range(weekField.weekOfYear()).getMaximum();
case LAST_10_YEARS:
return 10;
default:
throw new IllegalArgumentException("Unknown TimeRangeType!");
}
}
/**
* Creates the TimePeriod to be displayed in the TimeSeries graph for the
* specified time step.
*
* @param timeType the time range type to be displayed
* @param year the year to be displayed
* @param timeStep the current time step in the graph
* @return the created TimePeriod for the current time step
*/
private RegularTimePeriod createTimePeriodForTimeStep(TimeRangeType timeType, int year, int timeStep) {
switch (timeType) {
case LAST_12_MONTHS:
LocalDate now = LocalDate.now();
int tempMonth = now.getMonthValue() + timeStep - 1;
int tempYear = now.getYear() - 1 + tempMonth / 12;
return new Month(tempMonth % 12 + 1, tempYear);
case MONTHS_OF_YEAR:
return new Month(timeStep + 1, year);
case WEEKS_OF_YEAR:
// Workaround for a JFreeChart problem: use the Year instead
// of Week class for the botton axis, otherwise there will be
// format problems on the axis (the first week is often "52")
int tempWeekNr = timeStep + 1;
return new Year(1900 + tempWeekNr);
case LAST_10_YEARS:
return new Year(year - 9 + timeStep);
default:
throw new IllegalArgumentException("Unknow TimeRangeType!");
}
}
/**
* Creates the EntryFilter with the time range (dateStart/dateEnd)
* to be shown in the TimeSeries graph for the specified time step.
* All the other EntryFilter attributes are not set yet.
*
* @param timeType the time range type to be displayed
* @param year the year to be displayed
* @param timeStep the current time step in the graph
* @return the created EntryFilter for the current time step
*/
private EntryFilter createExerciseFilterForTimeStep(TimeRangeType timeType, int year, int timeStep) {
EntryFilter filter = new EntryFilter();
LocalDate now = LocalDate.now();
LocalDate dateRangeStart;
LocalDate dateRangeEnd;
// setup time range of filter depending on the specified time type
switch (timeType) {
case LAST_12_MONTHS:
int tempMonth = now.getMonthValue() + timeStep - 1;
int tempYear = now.getYear() - 1 + tempMonth / 12;
dateRangeStart = LocalDate.of(tempYear, tempMonth % 12 + 1, 1);
dateRangeEnd = dateRangeStart.plusMonths(1).minusDays(1);
break;
case MONTHS_OF_YEAR:
tempMonth = timeStep;
dateRangeStart = LocalDate.of(year, tempMonth + 1, 1);
dateRangeEnd = dateRangeStart.plusMonths(1).minusDays(1);
break;
case WEEKS_OF_YEAR:
int tempWeekNr = timeStep + 1;
dateRangeStart = getStartDateForWeekOfYear(year, tempWeekNr);
dateRangeEnd = dateRangeStart.plusDays(6);
break;
case LAST_10_YEARS:
tempYear = year - 9 + timeStep;
dateRangeStart = LocalDate.of(tempYear, 1, 1);
dateRangeEnd = dateRangeStart.plusYears(1).minusDays(1);
break;
default:
throw new IllegalArgumentException("Unknow TimeRangeType!");
}
filter.setDateStart(dateRangeStart);
filter.setDateEnd(dateRangeEnd);
return filter;
}
/**
* Adds custom colors for all the diagram graphs, because some color presets are not usable or
* readable (if more colors are needed, then presets will be used).
*/
private void addCustomGraphColors(java.util.List<java.awt.Color> graphColors) {
graphColors.add(new java.awt.Color(0xff5555));
graphColors.add(new java.awt.Color(0x5555ff));
graphColors.add(new java.awt.Color(0x3acc2e));
graphColors.add(new java.awt.Color(0xff8000));
graphColors.add(new java.awt.Color(0xff55ff));
graphColors.add(new java.awt.Color(0x31d5d5));
graphColors.add(new java.awt.Color(0xdc8686));
graphColors.add(new java.awt.Color(0x808080));
}
private LocalDate getStartDateForWeekOfYear(int year, int weekNr) {
WeekFields weekField = getWeekFieldsForWeekStart();
// create date for some day in the specified year
LocalDate date = LocalDate.of(year, 2, 1);
// set the first weekday (values 1 - 7, value 1 is Sunday or Monday)
date = date.with(weekField.dayOfWeek(), 1);
// set the specified week number
return date.with(weekField.weekOfWeekBasedYear(), weekNr);
}
private WeekFields getWeekFieldsForWeekStart() {
// use ISO (week start at monday) or SUNDAY_START WeekFields depending on configuration
return document.getOptions().isWeekStartSunday() ? WeekFields.SUNDAY_START : WeekFields.ISO;
}
private boolean isExerciseFilterEnabled() {
return document.isFilterEnabled()
&& document.getCurrentFilter().getEntryType() == EntryFilter.EntryType.EXERCISE;
}
/**
* Merges the specified filter for time series creation with the existing filter
* in the SportsTracker view (if it is enabled).
*
* @param filter the filter used for time series creation
*/
private void mergeExerciseFilterIfEnabled(EntryFilter filter) {
// when the exercise filter is enabled in the GUI then we need to
// merge these filter criterias into the created diagram filter
if (isExerciseFilterEnabled()) {
EntryFilter currentFilter = document.getCurrentFilter();
// merge filter date
if (currentFilter.getDateStart().isAfter(filter.getDateStart())) {
filter.setDateStart(currentFilter.getDateStart());
}
if (currentFilter.getDateEnd().isBefore(filter.getDateEnd())) {
filter.setDateEnd(currentFilter.getDateEnd());
}
// merge sport type and subtype filter
if (currentFilter.getSportType() != null) {
if (filter.getSportType() != null && !currentFilter.getSportType().equals(filter.getSportType())) {
// filters have different sport types => add a not existing sport type, so nothing will be found
filter.setSportType(new SportType(Integer.MIN_VALUE));
} else {
filter.setSportType(currentFilter.getSportType());
if (currentFilter.getSportSubType() != null) {
filter.setSportSubType(currentFilter.getSportSubType());
}
}
}
// merge intensity filter
if (currentFilter.getIntensity() != null) {
filter.setIntensity(currentFilter.getIntensity());
}
// merge equipment filter
if (currentFilter.getEquipment() != null) {
if (filter.getEquipment() != null && !currentFilter.getEquipment().equals(filter.getEquipment())) {
// filters have different equipments => add a not existing equipment, so nothing will be found
filter.setEquipment(new Equipment(Integer.MIN_VALUE));
} else {
filter.setEquipment(currentFilter.getEquipment());
}
}
// merge comment filter
if (currentFilter.getCommentSubString() != null) {
filter.setCommentSubString(currentFilter.getCommentSubString());
filter.setRegularExpressionMode(currentFilter.isRegularExpressionMode());
}
}
}
/**
* Returns the average weight value for all Weight entries in the time range
* of the specified EntryFilter.
*
* @param filter the EntryFilter with the time range to be used
* @return the average weight value or 0 when no Weight entries found
*/
private double getAverageWeightInTimeRange(EntryFilter filter) {
java.util.List<Weight> weightsInTimeRange = document.getWeightList().getEntriesInDateRange(
filter.getDateStart(), filter.getDateEnd());
if (weightsInTimeRange.isEmpty()) {
return 0;
}
double weightSum = 0;
for (Weight weight : weightsInTimeRange) {
weightSum += weight.getValue();
}
return weightSum / (double) weightsInTimeRange.size();
}
/**
* This is the list of possible time ranges displayed in diagram.
* This enum also provides the localized displayed enum names.
*/
private enum TimeRangeType {
/**
* In total 13 months: current month and last 12 before (good
* for compare current month and the one from year before).
*/
LAST_12_MONTHS("st.dlg.overview.time_range.last_12_months.text"), //
MONTHS_OF_YEAR("st.dlg.overview.time_range.months_of_year.text"), //
WEEKS_OF_YEAR("st.dlg.overview.time_range.weeks_of_year.text"), //
LAST_10_YEARS("st.dlg.overview.time_range.ten_years.text");
private static AppResources appResources;
private String resourceKey;
private TimeRangeType(final String resourceKey) {
this.resourceKey = resourceKey;
}
@Override
public String toString() {
return appResources.getString(resourceKey);
}
}
/**
* This is the list of possible value types displayed in diagram.
* This enum also provides the localized displayed enum names.
*/
private enum ValueType {
DISTANCE("st.dlg.overview.display.distance_sum.text"), //
DURATION("st.dlg.overview.display.duration_sum.text"), //
ASCENT("st.dlg.overview.display.ascent_sum.text"), //
CALORIES("st.dlg.overview.display.calorie_sum.text"), //
EXERCISES("st.dlg.overview.display.exercise_count.text"), //
AVG_SPEED("st.dlg.overview.display.avg_speed.text"), //
SPORTSUBTYPE("st.dlg.overview.display.sportsubtype_distance.text"), //
EQUIPMENT("st.dlg.overview.display.equipment_distance.text"), //
WEIGHT("st.dlg.overview.display.weight.text");
private static AppResources appResources;
private String resourceKey;
private ValueType(final String resourceKey) {
this.resourceKey = resourceKey;
}
@Override
public String toString() {
return appResources.getString(resourceKey);
}
/**
* Returns the name of the value type including the current unit system, e.g. for displaying as axis name.
*
* @return name and unit system
*/
private String getNameWithUnitSystem(final FormatUtils formatUtils) {
switch (this) {
case DISTANCE:
return appResources.getString("st.dlg.overview.value_type.distance_sum",
formatUtils.getDistanceUnitName());
case DURATION:
return appResources.getString("st.dlg.overview.value_type.duration_sum");
case ASCENT:
return appResources.getString("st.dlg.overview.value_type.ascent_sum",
formatUtils.getAltitudeUnitName());
case CALORIES:
return appResources.getString("st.dlg.overview.value_type.calories_sum");
case EXERCISES:
return appResources.getString("st.dlg.overview.value_type.exercise_count");
case AVG_SPEED:
return appResources.getString("st.dlg.overview.value_type.avg_speed",
formatUtils.getSpeedUnitName());
case SPORTSUBTYPE:
return appResources.getString("st.dlg.overview.value_type.sportsubtype_distance",
formatUtils.getDistanceUnitName());
case EQUIPMENT:
return appResources.getString("st.dlg.overview.value_type.equipment_distance",
formatUtils.getDistanceUnitName());
case WEIGHT:
return appResources.getString("st.dlg.overview.value_type.weight", formatUtils.getWeightUnitName());
default:
throw new IllegalArgumentException("Invalid value type!");
}
}
}
/**
* This is the list of possible overview types to draw in diagram.
* This enum also provides the localized displayed enum names.
*/
private enum OverviewType {
EACH_SPLITTED("st.dlg.overview.sport_type.each_splitted.text"), //
EACH_STACKED("st.dlg.overview.sport_type.each_stacked.text"), //
ALL_SUMMARY("st.dlg.overview.sport_type.all_summary.text"); //
private static AppResources appResources;
private String resourceKey;
private OverviewType(final String resourceKey) {
this.resourceKey = resourceKey;
}
@Override
public String toString() {
return appResources.getString(resourceKey);
}
}
}